package com.gitee.l0km.aocache;

import java.nio.file.StandardOpenOption;
import java.nio.charset.StandardCharsets;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.channels.OverlappingFileLockException;
import java.io.IOException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;

import org.aspectj.lang.JoinPoint;

import com.gitee.l0km.aocache.config.AoCacheGlobalConfig;

/**
 * 性能日志记录器，用于异步记录方法执行性能数据并持久化为CSV格式。<br>
 * 
 * <b>实现机制：</b>
 * <ul>
 * <li> 使用守护线程（{@code aocache-csvLogger}）进行异步日志处理</li>
 * <li> 基于 {@link ConcurrentLinkedQueue} 实现线程安全的无锁队列存储性能记录</li>
 * <li> 定时刷新机制（默认5秒）通过 {@link Thread#sleep} 实现周期性写入</li>
 * <li> JVM关闭时自动触发最终数据持久化（通过 shutdown hook 实现）</li>
 * </ul>
 * 
 * <b>功能特性：</b>
 * <ul>
 * <li> 运行时动态控制：通过 {@link #setEnableLog(boolean)} 即时启停日志记录</li>
 * <li> 配置灵活性：支持系统参数动态配置（路径、间隔时间）</li>
 * <li> 容错机制：自动创建日志目录，异常写入失败时降级到控制台日志</li>
 * <li> 资源管理：使用 try-with-resources 确保文件句柄正确释放</li>
 * </ul>
 * 
 * <b>日志格式规范：</b>
 * <table border="1">
 * <tr><th>字段</th><th>类型</th><th>描述</th></tr>
 * <tr><td>JoinPoint</td><td>String</td><td>切入点全限定名（带包路径）</td></tr>
 * <tr><td>ArgsHash</td><td>Integer</td><td>参数哈希值（Arrays.deepHashCode）</td></tr>
 * <tr><td>Count</td><td>Long</td><td>方法调用次数</td></tr>
 * <tr><td>Cost</td><td>Long</td><td>执行耗时（毫秒）</td></tr>
 * <tr><td>Timestamp</td><td>ISO8601</td><td>记录产生时间（精确到毫秒）</td></tr>
 * </table>
 * 
 * <b>系统参数配置：</b>
 * <ul>
 * <li> {@code -Daocache.enableLog=true} 启用性能日志（默认关闭）</li>
 * <li> {@code -Daocache.logFolder=/path} 指定日志目录（默认：~/.aocache）</li>
 * <li> {@code -Daocache.flushInterval=5000} 刷新间隔（毫秒，默认5000）</li>
 * </ul>
 */
public class PerformanceProfiler implements Runnable {
    /**
     * 默认的刷新间隔常量<br>
     * 用于配置系统在写入性能分析日志时，设置一个固定的刷新间隔时间。
     */
    private static final long DEFAULT_FLUSH_INTERVAL = 10 * 1000; // 默认 10 秒
    /**
     * 默认的日志文件名前缀。<p>
     * 该前缀将用于生成性能日志文件的文件名，文件名将基于此前缀结合日期等信息生成。<br>
     * 例如，使用该前缀生成的文件名可能为 "aocache-perf-yyyyMMdd-log.csv"。<p>
     * 此常量可通过系统参数 "profiler.logPrefix" 进行动态配置，若未配置则使用此默认值。
     */
    private static final String DEFAULT_LOG_PREFIX = "aocache";
    /**
     * 控制台日志记录器实例，使用默认的 AocacheLogger<br>
     * 用于性能分析器的日志输出
     */
    private static final AocacheLogger consoleLogger = AocacheLogger.DEFAULT;

    /** 
     * 性能日志记录器是否启用。<br>
     * 可以通过命令行 -Daocache.enableLog=true 来开启性能日志记录。
     */
    static volatile boolean enableLog = false;
    
    /**
     * 控制是否在启动时清理旧的性能分析日志文件。<br>
     * 可以通过命令行 -Daocache.cleanLogFile=true 来启用此功能。
     */
    static volatile boolean cleanLogFile = false;

    /**
     * 缓存性能分析器的日志文件夹路径。<br>
     * 默认位于用户主目录下的 .aocache 文件夹中。
     * 用于存储性能分析相关的日志文件。
     */
    static String logFolder;

    /**
     * 性能分析日志写入间隔时间(毫秒)
     */
    private static volatile long flushInterval;

    /**
     * 性能分析日志文件名的前缀。<p>
     * 该前缀将用于生成性能日志文件的文件名，最终的文件名会基于此前缀结合日期等信息生成。<br>
     * 例如，使用该前缀生成的文件名可能为 "aocache-perf-yyyyMMdd-log.csv"。<p>
     * 此变量可通过系统参数 "profiler.logPrefix" 进行动态配置，若未配置则使用默认值 {@link #DEFAULT_LOG_PREFIX}。
     */
    private static String logPrefix;

    /**
     * 性能记录队列，用于存储方法执行的性能统计记录。
     * 采用线程安全的并发链表队列实现，支持多线程并发访问。
     */
    private static final ConcurrentLinkedQueue<AroundRecord> queue = new ConcurrentLinkedQueue<>();
    private static final AtomicBoolean running = new AtomicBoolean(true);
    private static final SimpleDateFormat ISO8601 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
    private static volatile Thread loggerThread;

    private static final AtomicBoolean shutdownHookAdded = new AtomicBoolean(false);

    static {
        // 使用AoCacheGlobalConfig获取配置
        setEnableLog(AoCacheGlobalConfig.getBoolean("profiler.enableLog", false));
        cleanLogFile = AoCacheGlobalConfig.getBoolean("profiler.cleanLogFile", false);
        logFolder = AoCacheGlobalConfig.getString("profiler.logFolder", 
            Paths.get(System.getProperty("user.home"), ".aocache", "profiler").toString());
        flushInterval = AoCacheGlobalConfig.getLong("profiler.flushInterval", DEFAULT_FLUSH_INTERVAL);
        if(flushInterval <= 0) {
            flushInterval = DEFAULT_FLUSH_INTERVAL;
        }
        logPrefix = AoCacheGlobalConfig.getString("profiler.logPrefix", DEFAULT_LOG_PREFIX);
    }

    /**
     * 添加JVM关闭钩子(如果尚未添加)。<br>
     * 该钩子在JVM关闭时会调用 {@link #stopAndFlush()} 方法来确保性能数据被正确记录。<br>
     * 使用CAS操作确保钩子只被添加一次。
     */
    static void addShutdownHookIfNot() {
        if (shutdownHookAdded.compareAndSet(false, true)) {
            Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("AOCACHE:csvLogger shutdown hook");
                    stopAndFlush();
                }
            }));
        }
    }

    /**
     * 停止性能日志记录并刷新缓存数据<br>
     * 
     * 该方法执行两个操作：<br>
     * 1. 将运行状态标记设置为false，停止日志记录<br>
     * 2. 调用flushAndClose方法刷新剩余数据并关闭资源<br>
     * 
     */
    private static void stopAndFlush() {
        running.set(false);
        flushAndClose();
    }
    /**
     * 启动性能日志记录线程。<br>
     * 该方法创建一个新的线程，并将其设置为守护线程。<br>
     * 然后，将运行状态标记设置为true，并启动线程。<br>
     * 该方法是线程安全的。<br>
     */
    private static void startLoggingThread() {
        setRunning(true);
        loggerThread = new Thread(new PerformanceProfiler(), "aocache-csvLogger");
        loggerThread.setDaemon(true);
        loggerThread.start();
    }
    /**
     * 停止性能日志记录线程并等待其结束。<br>
     * 该方法首先将运行状态标记设置为false，然后等待日志记录线程结束。<br>
     * 最后，将日志记录线程引用设置为null。<br>
     * 该方法是线程安全的。<br>
     */
    private static void stopLoggingThread() {
        setRunning(false);
        try {
            if (loggerThread != null) {
                loggerThread.join();
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            consoleLogger.log("Interrupted while waiting for loggerThread to finish: {}", e.getMessage());
        }
        stopAndFlush();
        loggerThread = null;
    }

    /**
     * 设置是否启用性能日志记录功能，默认关闭<br>
     * 当传入的标志与当前启用状态不同时，执行相应的开启或关闭操作。
     * 若启用日志记录且线程未启动，则创建并启动新线程；若禁用日志记录且线程正在运行，
     * 则等待线程结束并刷新数据后再将线程引用置为 null。
     * 该方法是线程安全的。
     * 
     * @param flag true表示启用日志记录，false表示禁用日志记录
     */
    public static synchronized void setEnableLog(boolean flag) {
        if (flag != enableLog) {
            enableLog = flag;
            if (flag) {
                if (loggerThread == null || !loggerThread.isAlive()) {
                    startLoggingThread();
                }
            } else {
                if (loggerThread != null && loggerThread.isAlive()) {
                    stopLoggingThread();
                }
            }
        }
    }
    
    /**
     * 设置日志文件保存文件夹,默认为 ${user.home}/.aocache<br>
     * 
     * @param path 日志文件的路径，不能为null或空字符串
     * @throws IllegalArgumentException 当path参数为null或空字符串时抛出
     */
    public static synchronized void setLogFolder(String path) {
        if (path == null || path.isEmpty()) {
            throw new IllegalArgumentException("logPath cannot be null or empty");
        }
        logFolder = path;
    }

    /**
	 * 设置日志写入间隔时间（毫秒）
	 * @param interval 新的日志写入间隔时间，必须大于0
	 * @throws IllegalArgumentException 当间隔时间为负数时抛出
	 */
	public static synchronized void setFlushInterval(long interval) {
	    if (interval <= 0) {
	        throw new IllegalArgumentException("Interval must be non-negative");
	    }
	    flushInterval = interval;
    }

    /**
     * 设置是否在第一次写入文件时删除已经存在的日志文件，默认：false<br>
     * 
     * @param cleanLogFile true表示清理日志文件，false表示不清理
     */
    public static void setCleanLogFile(boolean cleanLogFile) {
        PerformanceProfiler.cleanLogFile = cleanLogFile;
	}

	/**
     * 添加一条性能记录到队列中
     * 
     * @param joinPoint 切点信息
	 * @param argsHash  参数哈希值
	 * @param count     执行次数
	 * @param cost      执行耗时
	 * @param computeCost 计算调用纯耗时, 缓存命中时为0
	 * @param timestamp 时间戳
     */
    static void addRecord(JoinPoint joinPoint, int argsHash, long count, long cost, long computeCost, Date timestamp) {
        if (enableLog) {
            queue.offer(new AroundRecord(joinPoint.toLongString(), argsHash, count, cost, computeCost, timestamp));
        }
    }

    /**
     * 获取性能日志文件名
     * 生成格式为 "aocache-perf-yyyyMMdd-log.csv" 的文件名,其中日期部分使用当前日期
     * 
     * @return 返回性能日志文件名
     */
    private static String logFileName() {
        return String.format(logPrefix + "-perf-%s-log.csv", new SimpleDateFormat("yyyyMMdd").format(new Date()).toString());
    }

    /**
     * 将缓存队列中的性能记录数据刷新到日志文件并关闭文件。<br>
     * 如果日志文件不存在,会创建文件并写入CSV格式的标题行。<br>
     * 该方法是同步的,以确保线程安全的文件写入。<br>
     * 
     * 日志文件格式为CSV,包含以下列:<br>
     * <ul>
     * <li> JoinPoint: 切入点信息</li>
     * <li> ArgsHash: 参数哈希值</li>
     * <li> Count: 调用次数</li>
     * <li> NanoCost: 执行耗时(纳秒)</li>
     * <li> Timestamp: 时间戳</li>
     * </ul>
     */
    private static synchronized void flushAndClose() {
        Path logDir = Paths.get(logFolder);
        if (!Files.exists(logDir)) {
            try {
                Files.createDirectories(logDir);
            } catch (IOException e) {
                consoleLogger.log("WARNING:Failed to create log folder: {}", e.getMessage());
                return;
            }
        }
        Path logPath = Paths.get(logFolder, logFileName());
        
        if (cleanLogFile) {
            try {
                Files.deleteIfExists(logPath);
                consoleLogger.log("Deleted log file: {}", logPath);
            } catch (IOException e) {
                consoleLogger.log("WARNING:Failed to delete log file: {}", e.getMessage());
            }
            cleanLogFile = false; // 确保只执行一次
        }
        FileChannel channel = null;
        FileLock lock = null;
        final int MAX_RETRIES = 5;
        int attempt = 0;

        while (attempt++ < MAX_RETRIES) {
            try {
                channel = FileChannel.open(logPath, 
                    StandardOpenOption.WRITE, 
                    StandardOpenOption.APPEND, 
                    StandardOpenOption.CREATE_NEW);
            } catch (FileAlreadyExistsException e) {
                try {
                    channel = FileChannel.open(logPath, 
                        StandardOpenOption.WRITE, 
                        StandardOpenOption.APPEND);
                } catch (IOException ex) {
                    consoleLogger.log("ERROR:Failed to open log file: {}", ex.getMessage());
                    return;
                }
            } catch (IOException e) {
                consoleLogger.log("ERROR:Failed to create log file: {}", e.getMessage());
                return;
            }

            try {
                lock = channel.tryLock();
                if (lock != null) {
                    // 获取锁后再次检查文件状态
                    long fileSize = channel.size();
                    if (fileSize == 0) {
                        channel.write(StandardCharsets.UTF_8.encode("JoinPoint,ArgsHash,Count,Cost,ComputeCost,Timestamp\n"));
                    }

                    // 写入队列数据
                    AroundRecord record;
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    while ((record = queue.poll()) != null) {
                        String line = record.formatRecordLine();
                        buffer.clear();
                        buffer.put(line.getBytes(StandardCharsets.UTF_8));
                        buffer.flip();
                        while (buffer.hasRemaining()) {
                            channel.write(buffer);
                        }
                    }
                    break;
                }
            } catch (OverlappingFileLockException | IOException e) {
                consoleLogger.log("WARNING:File lock attempt {} failed: {}", attempt, e.getMessage());
            } finally {
                try {
                    if (lock != null) {
                        lock.close();
                    }
                    if (channel != null) {
                        channel.close();
                    }
                } catch (IOException e) {
                    consoleLogger.log("ERROR:Failed to close resources: {}", e.getMessage());
                }
            }

            try {
                Thread.sleep(100 * attempt);
            } catch (InterruptedException e) {
                break;
            }
        }
    }

    private static void setRunning(boolean running) {
        PerformanceProfiler.running.set(running);
    }

    @Override
    public void run() {
        addShutdownHookIfNot();
        while(running.get()) {
            try {
                flushAndClose();
                Thread.sleep(flushInterval);
            } catch (InterruptedException e) { /* 处理中断 */}
        }
        consoleLogger.log("{} thread end", Thread.currentThread().getName());
    }

    /**
     * 记录切面执行的相关信息的数据类<br>
     * 
     * 包含以下信息:<br>
     * <ul>
     * <li> 切点信息(joinPoint)</li>
     * <li> 参数哈希值(argsHash)</li>
     * <li> 执行次数(count)</li>
     * <li> 耗时(cost)</li>
     * <li> 计算调用纯耗时(computeCost)</li>
     * <li> 时间戳(timestamp)</li>
     * </ul>
     * 提供格式化记录行的方法,用于生成CSV格式的日志记录
     */
    private static class AroundRecord {
        final String joinPoint;
        final int argsHash;
        final long count;
        final long cost;
        final long computeCost;
        final Date timestamp;

        private AroundRecord(String joinPoint, int argsHash, long count, long cost, long computeCost, Date timestamp) {
            this.joinPoint = joinPoint;
            this.argsHash = argsHash;
            this.count = count;
            this.cost = cost;
            this.computeCost = computeCost;
            this.timestamp = timestamp;
        }
        private String formatRecordLine() {
            return String.format("\"%s\",%d,%d,%d,%s,%s\n",
                joinPoint,
                argsHash,
                count,
                cost,
                computeCost,
                ISO8601.format(timestamp)
            );
        }
    }
}